import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Database1 {

    private Row rows[] = new Row[100];
    private ReentrantReadWriteLock[] readWriteLocks = new ReentrantReadWriteLock[100];
//    private Queue<Integer> writeLocks = new LinkedList<Integer>();
//    private Queue<Integer> readLocks = new LinkedList<Integer>();
    // TODO: is it necessary to use a queue to store locked rows?

    public Database1(){
        for (int i=0; i<100; i++){
               rows[i] = new Row(i);
        }
        for (int i=0; i<100; i++){
            readWriteLocks[i] = new ReentrantReadWriteLock();
        }
    }

    public void executeTransactions(List<Transaction> transactions){
        List<Thread> threads = new LinkedList<>();
        //Here I provide a serial implementation. You need to change it to a concurrent execution.
        for(Transaction t : transactions){
            Thread thread = new Thread(() -> {
                executeTransactionRunnable(t);
            });
            threads.add(thread);
            thread.start();
        }

        for (Thread t : threads){
            try {
                t.join();
            } catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    // runnable for each transaction
    public void executeTransactionRunnable(Transaction transaction){
        // traver the operations in the transaction
        for (Operation o : transaction.getOperations()){
            // read operation
            if (o.getType() == 0){
                // apply reentrant read lock to the row
                readWriteLocks[o.getRowNumber()].readLock().lock();
                o.setValue(rows[o.getRowNumber()].getValue());
            } else {
                // write operation
                // apply reentrant write lock to the row
                readWriteLocks[o.getRowNumber()].writeLock().lock();
                rows[o.getRowNumber()].setValue(o.getValue());
            }
            System.out.println("Transaction" + transaction.getId() + " " + o);
        }
        // release the locks
        for (Operation o : transaction.getOperations()){
            if (o.getType() == 0){
                readWriteLocks[o.getRowNumber()].readLock().unlock();
            } else {
                readWriteLocks[o.getRowNumber()].writeLock().unlock();
            }
        }
    }

    public static void main(String []args){
	    Transaction t1 = new Transaction(1);
        t1.addOperation(new Operation(0, 9, 0));
        t1.addOperation(new Operation(1, 54, 5));
        t1.addOperation(new Operation(0, 21, 0));
        
        Transaction t2 = new Transaction(2);
        t2.addOperation(new Operation(1, 9, 99));
        t2.addOperation(new Operation(0, 54, 0));
        t2.addOperation(new Operation(1, 21, 1234));

        Transaction t3 = new Transaction(3);
        t3.addOperation(new Operation(0, 1, 0));
        t3.addOperation(new Operation(0, 73, 0));
        t3.addOperation(new Operation(1, 54, 45));

        LinkedList<Transaction> batch = new LinkedList<Transaction>();
        batch.add(t2);
        batch.add(t1);
        batch.add(t3);

        Database1 db = new Database1();
        Database dbSerial = new Database();

        // Generate all permutations
        List<List<Transaction>> allPermutations = Permutations.generate(new ArrayList<>(batch));
        // TEST: print all permutations
//        for (List<Transaction> transactions : allPermutations){
//            for (Transaction t : transactions){
//                System.out.print(t.getId() + " -> ");
//            }
//            System.out.println("END");
//        }

        // parallel execute the batch
        db.executeTransactions(batch);

        // TEST: print the end state of the database
//        for (int i=0; i<100; i++){
//            System.out.println("Row " + i + " = " + db.rows[i].getValue());
//        }
//        System.out.println("Row 9 = " + db.rows[9].getValue());
//        System.out.println("Row 54 = " + db.rows[54].getValue());
//        System.out.println("Row 21 = " + db.rows[21].getValue());

        // serially execute all permutations, one by one
        for (List<Transaction> transactions : allPermutations){
            dbSerial.executeTransactions(transactions);

            // check if the execution has teh same end state as the serial execution
            boolean equivalent = true;
            for (int i=0; i<100; i++){
                if (db.rows[i].getValue() != dbSerial.getRows()[i].getValue()){
                    equivalent = false;
                    // TEST: print where the comparison fails
                    // System.out.println("Row " + i + " = (parallel)" + db.rows[i].getValue() + " != (serial)" + dbSerial.getRows()[i].getValue());
                    break;
                }
            }

            // if yes, print the equivalent message
            if (equivalent){
                System.out.print("This execution is equivalent to a serial execution of ");
                for (Transaction t : transactions) {
                    System.out.print("Transaction " + t.getId() + " -> ");
                }
                System.out.println("END");
            }
            // reset the serial database
            for (int i=0; i<100; i++){
                dbSerial.getRows()[i] = new Row(i);
            }
        }

    }
}
